﻿//////////////////////////////////////////////////////////////////////////////
//                                                                          //
// Copyright 2010 ZGDev Software                                            //
//                                                                          //
// Licensed under the Apache License, Version 2.0 (the "License");          //
// you may not use this file except in compliance with the License.         //
// You may obtain a copy of the License at                                  //
//                                                                          //
//   http://www.apache.org/licenses/LICENSE-2.0                             //
//                                                                          //
// Unless required by applicable law or agreed to in writing, software      //
// distributed under the License is distributed on an "AS IS" BASIS,        //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and      //
// limitations under the License.                                           //
//                                                                          //
//////////////////////////////////////////////////////////////////////////////



using System;
using System.Threading;
using System.Collections.Generic;

/// <summary>
/// FormSpark is an API that simplifies the animation of the properties of objects.
/// </summary>
namespace ZGDev.FormSpark
{
    /// <summary>
    /// A utility class that handles the timing and threading of animation contexts.
    /// </summary>
    /// <seealso cref="AnimationContext"/>
    public static class Animator
    {
        private static Thread m_runner;
        private static volatile bool m_running = false;
        private static Dictionary<string, AnimationContext> m_anims = new Dictionary<string, AnimationContext>();

        #region ManualInit

        /// <summary>
        /// "Manually" initializes the animator. This should be called by non-UI applications.
        /// </summary>
        public static void ManualInit()
        {
            if (m_runner == null)
            {
                AnimationContext.DefaultRequiresMainThread = false;

                m_runner = new Thread(new ThreadStart(run));
                m_running = true;
                m_runner.Start();
            }
        }

        #endregion

        #region InitWithControl

        /// <summary>
        /// Initializes the animator for a Windows Forms application.
        /// </summary>
        /// <param name="ctrl">A control that lives for the duration of the entire application (e.g. the main window).</param>
        public static void InitWithControl(System.Windows.Forms.Control ctrl)
        {
            MainThreadCallerControl mtcc = new MainThreadCallerControl();
            ctrl.Controls.Add(mtcc);

            System.Windows.Forms.Application.ApplicationExit += new EventHandler(ApplicationExiting);

            ManualInit();
            AnimationContext.DefaultRequiresMainThread = true;
        }

        #endregion

        #region InitWithWPFPanel

        /// <summary>
        /// Initializes the animator for a WPF application.
        /// </summary>
        /// <param name="ctrl">
        /// A control that lives for the duration of the entire application (e.g. the main window's content panel).
        /// </param>
        public static void InitWithWPFPanel(System.Windows.Controls.Panel ctrl)
        {
            MainThreadCallerWPFControl mtcc = new MainThreadCallerWPFControl();
            ctrl.Children.Add(mtcc);

            System.Windows.Application.Current.Exit += new System.Windows.ExitEventHandler(WPFApplicationExiting);

            ManualInit();
            AnimationContext.DefaultRequiresMainThread = true;
        }

        #endregion

        #region Exiting methods

        /// <summary>
        /// Should be called by non-UI applications when the application is terminating.
        /// </summary>
        public static void ManualExit()
        {
            if (m_runner != null)
            {
                Dispatch.MainThreadDispatcher = null;
                m_running = false;

                if (!m_runner.Join(500))
                {
                    m_runner.Abort();
                }

                m_runner = null;
            }
        }

        /// <summary>
        /// Called by the .NET runtime when the application is ending.
        /// </summary>
        /// <param name="sender">The event sender.</param>
        /// <param name="e">The event.</param>
        private static void ApplicationExiting(object sender, EventArgs e)
        {
            ManualExit();
        }

        /// <summary>
        /// Called by the .NET runtime when the application is ending.
        /// </summary>
        /// <param name="sender">The event sender.</param>
        /// <param name="e">The event.</param>
        private static void WPFApplicationExiting(object sender, System.Windows.ExitEventArgs e)
        {
            ManualExit();
        }

        #endregion

        #region Run

        /// <summary>
        /// Runs an animation context with a given name. Any animation with the same name
        /// will be replaced.
        /// </summary>
        /// <param name="name">The name of the animation.</param>
        /// <param name="context">The animation context.</param>
        /// <remarks>
        /// As the list of animations must be locked before the animation
        /// can be added, this task is grouped into a work item and dispatched
        /// to the thread pool to prevent the calling thread from hanging
        /// while a lock is acquired.
        /// </remarks>
        public static void Run(string name, AnimationContext context)
        {
            Dispatch.Run(() =>
            {
                lock (m_anims)
                {
                    m_anims[name] = context;
                }
            });
        }

        #endregion

        #region IsAnimationRunning

        /// <summary>
        /// Determines if an animation is running.
        /// </summary>
        /// <param name="name">The name of the animation.</param>
        /// <returns>
        /// True if the animation is running, or false if the animation has completed or doesn't exist.
        /// </returns>
        public static bool IsAnimationRunning(string name)
        {
            AnimationContext context = null;

            if (!m_anims.TryGetValue(name, out context))
            {
                return false;
            }

            return !context.Completed;
        }

        #endregion

        #region CancelAnimation

        /// <summary>
        /// Cancels an anmiation with a given name if it exists.
        /// </summary>
        /// <param name="name">The name of the animation.</param>
        /// <remarks>
        /// As the list of animations must be locked before the animation
        /// can be removed, this task is grouped into a work item and dispatched
        /// to the thread pool to prevent the calling thread from hanging
        /// while a lock is acquired.
        /// </remarks>
        public static void CancelAnimation(string name)
        {
            Dispatch.Run(() =>
            {
                lock (m_anims)
                {
                    if (m_anims.ContainsKey(name))
                    {
                        m_anims.Remove(name);
                    }
                }
            });
        }

        #endregion

        #region run

        /// <summary>
        /// This is our "run" method for the animation thread.
        /// </summary>
        private static void run()
        {
            DateTime then = DateTime.Now, now = DateTime.Now;
            
            // while the animation thread should running
            while (m_running)
            {
                // calculate the time delta since the last iteration of our loop in seconds
                now = DateTime.Now;
                float delta = (float)(now - then).Milliseconds / 1000.0f;
                then = now;

                // get a lock on the list of animations
                lock (m_anims)
                {
                    // TODO: there has to be a better way to do this... is there an iterator
                    // that allows us to remove objects as we iterate?
                    List<string> keysToRemove = new List<string>(5);

                    // check each animation in our list of animations
                    foreach (string key in m_anims.Keys)
                    {
                        AnimationContext context = m_anims[key];

                        // if it has completed and we shouldn't keep it after it has been completed,
                        // add it to our list of animations to remove
                        if (context.Completed && !context.KeepAfterCompleted)
                        {
                            keysToRemove.Add(key);
                        }
                        else
                        {
                            // we throw every context update out to the threadpool (allows for better performance
                            // than just trying to animate everything on a single thread)
                            Dispatch.Run(() =>
                            {
                                context.Update(delta);
                            });
                        }
                    }

                    // remove animations from our list if needed
                    if (keysToRemove.Count > 0)
                    {
                        foreach (string key in keysToRemove)
                        {
                            m_anims.Remove(key);
                        }
                    }
                }
                
                // let some other threads do some work for a while
                Thread.Sleep(25);
            }
        }

        #endregion
    }
}
